package org.datascooter.bundle;

import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.logging.Logger;

import org.datascooter.bundle.attribute.PersistAttribute;
import org.datascooter.bundle.attribute.PersistReferenceAttribute;
import org.datascooter.exception.EntityNotMappedException;
import org.datascooter.inface.IBundleChangeListener;
import org.datascooter.inface.IBundleProvider;
import org.datascooter.utils.DSSettings;
import org.datascooter.utils.LangUtils;

/**
 * Purposed for mapping management . EntityMapper contains and manage of all system mappings
 * 
 * Each class can be mapped many times only entity is unique.
 * 
 * @author nemo
 * 
 */
public final class DSMapper {

	private static Map<String, EntityBundle> bundleMap = new ConcurrentHashMap<String, EntityBundle>();
	private static BlockingQueue<EntityBundle> bundleList = new LinkedBlockingDeque<EntityBundle>();
	private static BlockingQueue<IBundleProvider> providerList = new LinkedBlockingDeque<IBundleProvider>();
	private static BlockingQueue<IBundleChangeListener> changeListeners = new LinkedBlockingDeque<IBundleChangeListener>();
	private static IBundleProvider defaultProvider = new FieldMappingProvider();
	private static DSMapper instance = new DSMapper();
	private static BlockingQueue<CrossTable> crossList = new LinkedBlockingDeque<CrossTable>();
	private static Map<String, List<PersistReferenceAttribute>> unresolvedRefs = new ConcurrentHashMap<String, List<PersistReferenceAttribute>>();

	private DSMapper() {
		super();
	}

	/**
	 * Returns a bundle by entity name
	 * 
	 * @throws NoSuchMethodException
	 * @throws SecurityException
	 */
	@SuppressWarnings("nls")
	public static EntityBundle getBundle(Object obj) throws EntityNotMappedException {
		String entity = LangUtils.entityName(obj);
		EntityBundle bundle = bundleMap.get(entity);
		if (bundle == null) {
			try {
				for (IBundleProvider provider : providerList) {
					bundle = provider.getBundle(obj);
					if (bundle != null) {
						if (!bundle.isVolatile) {
							addBundle(entity, bundle);
						}
						return bundle;
					}
				}
				if (DSSettings.isUseDefaultProvider()) {
					Logger.getLogger(DSMapper.class.getName()).info("***DefaultProvider Reflection Parse :   " + entity);
					bundle = defaultProvider.getBundle(obj);
					if (!bundle.isVolatile) {
						addBundle(entity, bundle);
					}
				}
			} catch (Exception e) {
				throw new EntityNotMappedException(entity, e);
			}
			if (bundle == null) {
				throw new EntityNotMappedException(entity, bundleMap);
			}
		}
		return bundle;
	}

	public static boolean isMapped(Object obj) throws EntityNotMappedException {
		return bundleMap.get(LangUtils.entityName(obj)) == null ? false : true;
	}

	public static void addBundle(String bundleKey, EntityBundle bundle) throws EntityNotMappedException {
		if (!bundleMap.containsKey(bundleKey)) {
			addBundleSp(bundleKey, bundle);
			fireBundleChanged(bundle);
		}
	}

	public static void addBundleInt(EntityBundle bundle) throws EntityNotMappedException {
		if (!bundleMap.containsKey(bundle.entity)) {
			addBundleSp(bundle.entity, bundle);
		}
	}

	private static void addBundleSp(String bundleKey, EntityBundle bundle) throws EntityNotMappedException {
		bundleMap.put(bundleKey, bundle);
		bundleList.add(bundle);
		bundle.complete();
		for (EntityBundle child : bundle.embeddedEntityList) {
			if (!bundleMap.containsKey(child.entity)) {
				bundleMap.put(child.entity, child);
				bundleList.add(child);
				child.complete();
			}
		}
		provideRefs(bundle);
	}

	public static void provideRefs(EntityBundle bundle) throws EntityNotMappedException {
		provideUnresolved(bundle);
		provideRefsInt(bundle, bundle.refEntityMap.values());
		List<PersistReferenceAttribute> list = new ArrayList<PersistReferenceAttribute>();
		for (PersistAttribute attribute : bundle.attrArray) {
			if (PersistReferenceAttribute.class.isAssignableFrom(attribute.getClass())) {
				list.add((PersistReferenceAttribute) attribute);
			}
		}
		provideRefsInt(bundle, list);
	}

	private static void provideUnresolved(EntityBundle bundle) {
		List<PersistReferenceAttribute> list = unresolvedRefs.get(bundle.entity);
		if (list != null) {
			for (PersistReferenceAttribute attribute : list) {
				attribute.setType(bundle.id.getType());
				attribute.setScale(bundle.id.getScale());
				attribute.setPrecision(bundle.id.getPrecision());
				bundle.addReferrer(attribute);
			}
			unresolvedRefs.remove(bundle.entity);
		}
	}

	private static void provideRefsInt(EntityBundle bundle, Collection<PersistReferenceAttribute> values) throws EntityNotMappedException {
		for (PersistReferenceAttribute attribute : values) {
			if (attribute.getType() == null) {
				EntityBundle bundle2 = bundleMap.get(attribute.getEntity());
				if (bundle2 != null) {
					attribute.setType(bundle2.id.getType());
					attribute.setScale(bundle2.id.getScale());
					attribute.setPrecision(bundle2.id.getPrecision());
					bundle2.addReferrer(attribute);
				} else {
					List<PersistReferenceAttribute> list2 = unresolvedRefs.get(attribute.getEntity());
					if (list2 == null) {
						list2 = new ArrayList<PersistReferenceAttribute>();
						unresolvedRefs.put(attribute.getEntity(), list2);
					}
					list2.add(attribute);
				}
			}
		}
	}

	public static void addBundleChangeListener(IBundleChangeListener listener) {
		changeListeners.add(listener);
	}

	public static void fireBundleChanged(EntityBundle bundle) {
		for (IBundleChangeListener listener : changeListeners) {
			listener.bundleChanged(bundle);
		}
	}

	/**
	 * Returns Id attributre for entity
	 * 
	 * @throws NoSuchMethodException
	 * @throws SecurityException
	 */
	public static PersistAttribute getId(String entity) throws EntityNotMappedException {
		PersistAttribute id = getBundle(entity).id;
		if (id == null) {
			return null;
		}
		return id;
	}

	public static Object getIdValue(Object obj) throws EntityNotMappedException, IllegalArgumentException, IllegalAccessException, InvocationTargetException {
		return getBundle(obj).getId(obj);
	}

	public static List<EntityBundle> getBundleList() {
		return new ArrayList<EntityBundle>(bundleList);
	}

	public static void clear() {
		providerList.clear();
		changeListeners.clear();
		bundleList.clear();
		bundleMap.clear();
	}

	/**
	 * Adds a bundle provider - it may be xml parser or other storage of mapping information - by example database
	 * 
	 * @throws Exception
	 */
	public static void addBundleProvider(IBundleProvider provider) throws Exception {
		providerList.add(provider);
		addBundles(provider);
	}

	public static void justAddBundleProvider(IBundleProvider provider) {
		providerList.add(provider);
	}

	/**
	 * Requests all bundle providers about available non volatile bundles. Some bundles may be volatile - can be changed during runtime
	 */
	public static void explore() throws Exception {
		for (IBundleProvider provider : providerList) {
			addBundles(provider);
		}
	}

	private static void addBundles(IBundleProvider provider) throws Exception {
		provider.explore();
		List<EntityBundle> list = provider.provideNonVolatile();
		for (EntityBundle bundle : list) {
			addBundle(bundle.entity, bundle);
		}
		crossList.addAll(provider.getCrossList());
	}

	public static void setBundleProviders(IBundleProvider[] bundleProviders) {
		for (IBundleProvider provider : bundleProviders) {
			justAddBundleProvider(provider);
		}
	}

	public static Map<String, EntityBundle> getBundleMap() {
		return bundleMap;
	}

	public static DSMapper getInstance() {
		return instance;
	}

	public static List<CrossTable> getCrossList() {
		return new ArrayList<CrossTable>(crossList);
	}
}
